#include "MFPlayer.h"
#include <mferror.h>
#include <chrono>

#include "CustomException.h"
#include "Tool.h"

namespace core::display::mf {
    namespace tool = util::tool;

    namespace ep = util::exception;

    UINT MFPlayer::mfplayer_count = 0;

    MFPlayer::MFPlayer(HWND playon) : 
    wnd(playon),
    ref_count(1),
    stats(static_cast<MFPlayStatus>(PlayStatus::Idle)),
    pSource(nullptr),
    pSession(nullptr),
    pVideoStream(nullptr),
    pDisplay(nullptr) {
        if(mfplayer_count <= 0)
            Initialize();
        mfplayer_count++;
    }

    void MFPlayer::Initialize(){
        HRESULT hr = MFStartup(MF_VERSION);
        if(!SUCCEEDED(hr)){
            throw ep::UnexpectedException("failed to Initialize MFStartup(), please check about whether lack of dependencies lies.\nhr:" + hr);
        }
    }

    HRESULT MFPlayer::Shutdown(){
        HRESULT hr = CloseSession();
        tool::SafeRelease(&pSource);
        tool::SafeRelease(&pSession);
        tool::SafeRelease(&pDisplay);
        if(pVideoStream != nullptr){
            pVideoStream->Close();
            pVideoStream->Release();
        }

        if(SUCCEEDED(hr) && mfplayer_count <= 0){
            hr = MFShutdown();
        }
        return hr;
    }

    MFPlayer::~MFPlayer(){
        mfplayer_count--;
        HRESULT hr = Shutdown();
        if(!SUCCEEDED(hr)){
            throw ep::UnexpectedException("error occurs when turn off media.\nhr code:" + hr);
        }
    }


    HRESULT MFPlayer::CreateMFPlayer(HWND wnd,MFPlayer ** ppPlayer){
        *ppPlayer = new MFPlayer(wnd);
        return S_OK;
    }

    ULONG MFPlayer::AddRef()
    {
        return InterlockedIncrement(&ref_count);
    }

    HRESULT MFPlayer::Paint(){
        if(stats != static_cast<MFPlayStatus>(PlayStatus::Playing))
            return S_FALSE;
        if(pDisplay == nullptr){
            return E_DRAW;
        }
        HRESULT hr = S_OK;
        PAINTSTRUCT ps;
        draw_wnd = BeginPaint(wnd, &ps);
        // Video is playing. Ask the player to repaint.
        hr = pDisplay->RepaintVideo();
        std::cout << "here" << std::endl;
        if(!SUCCEEDED(hr))
        {
            // The video is not playing, so we must paint the application window.
            ClearWhatItPaint();
        }
        else{
        }
        EndPaint(wnd, &ps);
        return hr;
    }

    ULONG MFPlayer::Release()
    {
        ULONG uCount = InterlockedDecrement(&ref_count);
        if (uCount <= 0)
        {
            delete this;
        }
        return uCount;
    }

    STDMETHODIMP MFPlayer::Invoke(IMFAsyncResult* pAsyncResult){
        MediaEventType meType = MEUnknown;  // Event type

        IMFMediaEvent *pEvent = NULL;

        // Get the event from the event queue.
        HRESULT hr = pSession->EndGetEvent(pAsyncResult, &pEvent);
        if (FAILED(hr))
        {
            goto release;
        }

        // Get the event type. 
        hr = pEvent->GetType(&meType);
        if (FAILED(hr))
        {
            goto release;
        }

        if (meType == MESessionClosed)
        {
            // The session was closed. 
            // The application is waiting on the m_hCloseEvent event handle. 
            // SetEvent(m_hCloseEvent);
        }
        else
        {
            // For all other events, get the next event in the queue.
            hr = pSession->BeginGetEvent(this, NULL);
            if (FAILED(hr))
            {
                goto release;
            }
        }

        // Check the application state. 
            
        // If a call to IMFMediaSession::Close is pending, it means the 
        // application is waiting on the m_hCloseEvent event and
        // the application's message loop is blocked. 

        // Otherwise, post a private window message to the application. 

        if (stats != MFPlayStatus::Closing)
        {
               // Leave a reference count on the event.
                pEvent->AddRef();
            SafeCall<HRESULT, IMFMediaEvent*>(
                [](AbstractPlayer *pPlayer, IMFMediaEvent *pe){
                    return dynamic_cast<MFPlayer *>(pPlayer)->HandleEvent(pe);
                },
                pEvent
            );
        }

    release:
        tool::SafeRelease(&pEvent);
        return hr;
    }

    HRESULT MFPlayer::HandleEvent(IMFMediaEvent* pEvent)
    {
        HRESULT hrStatus = S_OK;            
        MediaEventType meType = MEUnknown;  


        if (pEvent == NULL)
        {
            return E_POINTER;
        }

        // Get the event type.
        HRESULT hr = pEvent->GetType(&meType);
        if (FAILED(hr))
        {
            goto done;
        }

        // Get the event status. If the operation that triggered the event 
        // did not succeed, the status is a failure code.
        hr = pEvent->GetStatus(&hrStatus);

        // Check if the async operation succeeded.
        if (SUCCEEDED(hr) && FAILED(hrStatus)) 
        {
            hr = hrStatus;
        }
        if (FAILED(hr))
        {
            goto done;
        }

        switch(meType)
        {
        case MESessionTopologyStatus:
            hr = OnTopologyStatus(pEvent);
            break;

        case MEEndOfPresentation:
            hr = OnPresentationEnded(pEvent);
            break;
        case MESessionEnded:
            hr = OnSessionEnded(pEvent);
            break;

        case MENewPresentation:
            hr = OnNewPresentation(pEvent);
            break;

        default:
            hr = S_OK;
            // hr = OnSessionEvent(pEvent, meType);
            break;
        }

    done:
        tool::SafeRelease(&pEvent);
        return hr;
    }

    HRESULT MFPlayer::OnTopologyStatus(IMFMediaEvent *pEvent)
    {
        UINT32 status; 

        HRESULT hr = pEvent->GetUINT32(MF_EVENT_TOPOLOGY_STATUS, &status);
        if (SUCCEEDED(hr) && (status == MF_TOPOSTATUS_READY))
        {
            tool::SafeRelease(&pDisplay);

            // Get the IMFVideoDisplayControl interface from EVR. This call is
            // expected to fail if the media file does not have a video stream.

            (void)MFGetService(pSession, MR_VIDEO_RENDER_SERVICE, 
                IID_PPV_ARGS(&pDisplay));
            
            pDisplay->SetFullscreen(false);

            stats = static_cast<MFPlayStatus>(PlayStatus::Ready);
        }
        return hr;
    }

    //  Handler for MEEndOfPresentation event.
    HRESULT MFPlayer::OnPresentationEnded(IMFMediaEvent *pEvent)
    {
        // The session puts itself into the stopped state automatically.
        return S_OK;
    }

    HRESULT MFPlayer::OnSessionEnded(IMFMediaEvent *pEvent){
        stats = static_cast<MFPlayStatus>(PlayStatus::Stopped);
        if(current_lock_ptr != nullptr)
            has_stop.notify_all();
        InvalidateRect(wnd, NULL, false);
        //防止自锁
        current_lock_ptr->unlock();
        pListener->Expose()->Receive(msg::AM_PLAYER_STOPPED, nullptr);
        ClearWhatItPaint();
        return S_OK;
    }
    //  Handler for MENewPresentation event.
    //
    //  This event is sent if the media source has a new presentation, which 
    //  requires a new topology. 

    HRESULT MFPlayer::OnNewPresentation(IMFMediaEvent *pEvent)
    {
        return PrepareToPlay();
    }

    HRESULT MFPlayer::QueryInterface(REFIID riid, void** ppv)
    {
        static const QITAB qit[] = 
        {
            QITABENT(MFPlayer, IMFAsyncCallback),
            { 0 }
        };
        return QISearch(this, qit, riid, ppv);
    }

    HRESULT MFPlayer::PrepareSource(PCWSTR sUrl){
        MF_OBJECT_TYPE ObjectType = MF_OBJECT_INVALID;
        HRESULT hr;
        IMFSourceResolver * pResolver = nullptr;
        IUnknown * pReciver = nullptr;
        hr = MFCreateSourceResolver(&pResolver);
        if(!SUCCEEDED(hr))
            goto release;
        hr = pResolver->CreateObjectFromURL(
            sUrl,
            MF_RESOLUTION_MEDIASOURCE,
            NULL,
            &ObjectType,
            &pReciver
        );
        if(!SUCCEEDED(hr))
            goto release;
        hr = pReciver->QueryInterface(IID_PPV_ARGS(&pSource));
    release:
        tool::SafeRelease(&pResolver);
        tool::SafeRelease(&pReciver);
        return hr;
    }

    HRESULT MFPlayer::PrepareSource(res::BinFile *file){
        MF_OBJECT_TYPE ObjectType = MF_OBJECT_INVALID;
        HRESULT hr;
        ULONG wd_size;
        IMFAttributes * pAttr;
        IMFSourceResolver * pResolver = nullptr;
        IUnknown * pReciver = nullptr;
        if(pVideoStream != nullptr){
            pVideoStream->Close();
        }
        hr = MFCreateSourceResolver(&pResolver);
        if(!SUCCEEDED(hr))
            goto release;
        // hr = MFCreateTempFile(
        //     MF_ACCESSMODE_READWRITE,
        //     MF_OPENMODE_DELETE_IF_EXIST,
        //     MF_FILEFLAGS_NONE,
        //     &pStream
        // );

        hr = MFCreateFile(
            MF_ACCESSMODE_READWRITE,
            MF_OPENMODE_DELETE_IF_EXIST,
            MF_FILEFLAGS_NONE,
            L"temp.mp4",
            &pVideoStream
        );
        
        if(!SUCCEEDED(hr))
            goto release;
        hr = pVideoStream->Write(
            reinterpret_cast<const BYTE *>(file->data),
            file->size,
            &wd_size
        );
        if(!SUCCEEDED(hr))
            goto release;
        hr = pVideoStream->QueryInterface(IID_PPV_ARGS(&pAttr));
        if(!SUCCEEDED(hr))
            goto release;
        hr = pAttr->SetString(MF_BYTESTREAM_CONTENT_TYPE, L"video/mp4");
        if(!SUCCEEDED(hr))
            goto release;
        hr = pResolver->CreateObjectFromByteStream(
            pVideoStream,
            NULL,
            MF_RESOLUTION_MEDIASOURCE,
            NULL,
            &ObjectType,
            &pReciver
        );
        if(!SUCCEEDED(hr))
            goto release;
        hr = pReciver->QueryInterface(IID_PPV_ARGS(&pSource));
    release:
        tool::SafeRelease(&pResolver);
        tool::SafeRelease(&pReciver);
        tool::SafeRelease(&pAttr);
        return hr;
    }

    //  Create an activation object for a renderer, based on the stream media type.

    HRESULT MFPlayer::CreateMediaSinkActivate(
        IMFStreamDescriptor *pSourceSD,     // Pointer to the stream descriptor.
        HWND hVideoWindow,                  // Handle to the video clipping window.
        IMFActivate **ppActivate
    )
    {
        IMFMediaTypeHandler *pHandler = NULL;
        IMFActivate *pActivate = NULL;

        // Get the media type handler for the stream.
        HRESULT hr = pSourceSD->GetMediaTypeHandler(&pHandler);
        if (FAILED(hr))
            goto release;
        // Get the major media type.
        GUID guidMajorType;
        hr = pHandler->GetMajorType(&guidMajorType);
        if (FAILED(hr))
            goto release;
    
        // Create an IMFActivate object for the renderer, based on the media type.
        if (MFMediaType_Audio == guidMajorType)
        {
            // Create the audio renderer.
            hr = MFCreateAudioRendererActivate(&pActivate);
        }
        else if (MFMediaType_Video == guidMajorType)
        {
            // Create the video renderer.
            hr = MFCreateVideoRendererActivate(hVideoWindow, &pActivate);
        }
        else
        {
            // Unknown stream type. 
            hr = E_FAIL;
            // Optionally, you could deselect this stream instead of failing.
        }
        if (FAILED(hr))
            goto release;
    
        // Return IMFActivate pointer to caller.
        *ppActivate = pActivate;
        (*ppActivate)->AddRef();

    release:
        tool::SafeRelease(&pHandler);
        tool::SafeRelease(&pActivate);
        return hr;
    }

    //  Create a new instance of the media session.
    HRESULT MFPlayer::CreateSession()
    {
        // Close the old session, if any.
        HRESULT hr = CloseSession();
        if (FAILED(hr))
            goto release;

        // Create the media session.
        hr = MFCreateMediaSession(NULL, &pSession);
        if (FAILED(hr))
            goto release;

        // Start pulling events from the media session
        hr = pSession->BeginGetEvent((IMFAsyncCallback*)this, NULL);
        if (FAILED(hr))
            goto release;

    release:
        return hr;
    }

    HRESULT MFPlayer::CloseSession(){
        return S_OK;
    }

    HRESULT MFPlayer::PrepareTopology(IMFPresentationDescriptor** ppPd){
        if(pSession == nullptr || pSource == nullptr)
            return E_FAIL;
        pSession->ClearTopologies();
        HRESULT hr = S_OK;
        IMFStreamDescriptor ** arr_pSp = nullptr;
        DWORD stream_count = 0;
        IMFTopology * pTopology = nullptr;
        IMFTopologyNode * pSourceNode = nullptr;
        IMFTopologyNode * pOutputNode = nullptr;

        if(!SUCCEEDED(hr))
            goto release;
        hr =(*ppPd)->GetStreamDescriptorCount(&stream_count);
        arr_pSp = new IMFStreamDescriptor* [stream_count];
        if(!SUCCEEDED(hr))
            goto release;
        hr = MFCreateTopology(&pTopology);
        if(!SUCCEEDED(hr))
            goto release;

        for(DWORD i = 0; i < stream_count; i++){
            BOOL fselect = false;
            hr = (*ppPd)->GetStreamDescriptorByIndex(i, &fselect, arr_pSp + i);
            if(!SUCCEEDED(hr))
                goto release;
            if(fselect){
                IMFActivate * pActivate;
                DWORD stream_id;
                arr_pSp[i]->GetStreamIdentifier(&stream_id);
                hr = CreateMediaSinkActivate(
                    arr_pSp[i],
                    wnd,
                    &pActivate
                );
                if(SUCCEEDED(hr)){
                    hr = MFCreateTopologyNode(
                        MF_TOPOLOGY_SOURCESTREAM_NODE,
                        &pSourceNode
                    );
                }
                if(SUCCEEDED(hr))
                    hr = pSourceNode->SetUnknown(MF_TOPONODE_SOURCE, pSource);
                if(SUCCEEDED(hr))
                    hr = pSourceNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, (*ppPd));
                if(SUCCEEDED(hr))
                    hr = pSourceNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, arr_pSp[i]);

                if(SUCCEEDED(hr))
                    hr = MFCreateTopologyNode(
                        MF_TOPOLOGY_OUTPUT_NODE,
                        &pOutputNode
                    );
                if(SUCCEEDED(hr))
                    hr = pOutputNode->SetObject(pActivate);
                if (SUCCEEDED(hr))
                    hr = pOutputNode->SetUINT32(MF_TOPONODE_STREAMID, 0);
                if (SUCCEEDED(hr))
                    hr = pOutputNode->SetUINT32(MF_TOPONODE_NOSHUTDOWN_ON_REMOVE, FALSE);

                if (SUCCEEDED(hr))
                    hr = pTopology->AddNode(pSourceNode);
                if (SUCCEEDED(hr))
                    hr = pTopology->AddNode(pOutputNode);
                if (SUCCEEDED(hr))
                    hr = pSourceNode->ConnectOutput(0, pOutputNode, 0);
                tool::SafeRelease(&pActivate);
                tool::SafeRelease(&pSourceNode);
                tool::SafeRelease(&pOutputNode);
                if (FAILED(hr))
                    goto release;
            }
            else{
                std::cout << "Notice:\tone stream (index of " << stream_count << ") isn't selected";    
            }
        }
        if(!SUCCEEDED(hr))
            goto release;
        hr = pSession->SetTopology(
            MFSESSION_SETTOPOLOGY_IMMEDIATE,
            pTopology
        );
    release:
        for(DWORD i = 0; i < stream_count; i++)
            tool::SafeRelease(arr_pSp + i);
        delete[] arr_pSp;
        tool::SafeRelease(&pTopology);
        tool::SafeRelease(&pSourceNode);
        tool::SafeRelease(&pOutputNode);
        return hr;
    }

    HRESULT MFPlayer::PrepareToPlay(){
        HRESULT hr;
        IMFPresentationDescriptor * pPd = nullptr;
        DWORD stream_count;
        if(pSource == nullptr){
            return E_FAIL;
        }
        this->stats = MFPlayStatus::WaitForResponce;
        hr = CreateSession();
        if(!SUCCEEDED(hr))
            goto release;
            
        hr = pSource->CreatePresentationDescriptor(&pPd);
        if(!SUCCEEDED(hr))
            goto release;
        hr = pPd->GetStreamDescriptorCount(&stream_count);
        if(!SUCCEEDED(hr))
            goto release;
        for(DWORD i = 0; i < stream_count; i++){
            hr = pPd->SelectStream(i);
            if(!SUCCEEDED(hr))
                goto release;
        }

        hr = PrepareTopology(&pPd);
    release:
        tool::SafeRelease(&pPd);
        return hr;
    }

    HRESULT MFPlayer::StartPlayback()
    {
        PROPVARIANT varStart;
        PropVariantInit(&varStart);

        HRESULT hr = pSession->Start(NULL, &varStart);

        if(SUCCEEDED(hr)){
            stats = static_cast<MFPlayStatus>(PlayStatus::Playing);
        }
        PropVariantClear(&varStart);
        return hr;
    }

    HRESULT MFPlayer::Play(){
        HRESULT hr = S_OK;
        if(stats == static_cast<MFPlayStatus>(PlayStatus::Ready)){
            hr = this->StartPlayback();
        }
        else {
            return S_FALSE;
        }
        return hr;
    }

    HRESULT MFPlayer::Stop(){
        using namespace std::chrono;
        HRESULT hr = S_OK;
        if(stats == static_cast<MFPlayStatus>(PlayStatus::Playing)){
            hr = pSession->Stop();
            if(!SUCCEEDED(hr)){
                return hr;
            }
            if(current_lock_ptr != nullptr){
                if(!has_stop.wait_for(
                    *current_lock_ptr, seconds(5),
                    [this](){
                        return this->stats == static_cast<MFPlayStatus>(PlayStatus::Stopped);
                    })
                ){
                    throw ep::UnexpectedException("time out for stop media");   
                }
            }
            else {
                auto start = system_clock::now();
                while(1){
                    auto duration = system_clock::now() - start;
                    if(duration_cast<seconds>(duration).count() > 5){
                        throw ep::UnexpectedException("time out for stop media");   
                    }
                    else{
                        if(stats == static_cast<MFPlayStatus>(PlayStatus::Stopped)){
                            break;
                        }
                    }
                }
            }

        }
        else {
            return S_FALSE;
        }
        return hr;
    }

    void MFPlayer::Reset(){
        if(stats == static_cast<MFPlayStatus>(PlayStatus::Playing)){
            HRESULT hr = Stop();
            if(!SUCCEEDED(hr)){
                throw ep::UnexpectedException("error occered when stop the media\nhr:" + hr);
            }
        }
        if (stats == static_cast<MFPlayStatus>(PlayStatus::Stopped))
            this->stats = static_cast<MFPlayStatus>(PlayStatus::Ready);
    }

    
    void MFPlayer::ClearWhatItPaint(){
        RECT rc;
        GetClientRect(wnd, &rc);
        FillRect(draw_wnd, &rc, (HBRUSH) COLOR_WINDOW);
        UpdateWindow(wnd);
    }

    D2D1_RECT_F MFPlayer::GetRect(){
        RECT rect;
        bool flat = GetWindowRect(wnd,&rect);
        if(flat){
            return D2D1::RectF(rect.left,rect.top,rect.right,rect.bottom);
        }
        else {
            return D2D1::RectF();
        }
    }

    PlayStatus MFPlayer::GetStatus(){
        return stats > static_cast<MFPlayStatus>(PlayStatus::_) ? 
            PlayStatus::_ : static_cast<PlayStatus>(stats);
    }

    bool MFPlayer::GetCurrentFrameD2DBitmap(ID2D1Bitmap ** ppbmp){
        return false;
    }
}